package com.heima.schedule.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.common.constants.ScheduleConstants;
import com.heima.common.redis.CacheService;
import com.heima.model.schedule.dtos.Task;
import com.heima.model.schedule.pojos.Taskinfo;
import com.heima.model.schedule.pojos.TaskinfoLogs;
import com.heima.schedule.mapper.TaskinfoLogsMapper;
import com.heima.schedule.mapper.TaskinfoMapper;
import com.heima.schedule.service.TaskService;
import com.sun.org.apache.bcel.internal.generic.LREM;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Slf4j
@Service
@Transactional
public class TaskServiceImpl implements TaskService {

    /**
     * 添加延时任务
     * @param task
     * @return
     */
    @Override
    public long addTask(Task task) {
        //1.添加任务到数据库中
        boolean isOk =addTaskToDB(task);
        //添加任务到数据库成功之后
        if(isOk){
            //2.添加任务到redis中
            addTaskToCache(task);
        }

        return task.getTaskId();
    }

    /**
     * 取消任务
     * @param taskId
     * @return
     */
    @Override
    public boolean cancelTask(long taskId) {

        boolean flag = false;

        //1删除任务，更新任务日志，后期有可能要消费/删除任务，都要更新任务状态，所以集成到一起
        Task task = updateDb(taskId, ScheduleConstants.CANCELLED);
        //返回Task是因为后面删除redis的数据需要用到taskId
        if(task != null){
            //删除redis中的数据
            removeTaskFromCache(task);
            flag = true;
        }
        return flag;
    }

    /**
     * 按照类型和优先级拉取(拉出来后，会删掉)任务(消费任务)
     * @param type
     * @param priority
     * @return
     */
    @Override
    public Task poll(int type, int priority) {
        Task task = null;
        try{
            //这里别搞错不是取key的数据 是从名字为key的list 中拉取一条数据 数据的id不是key 装数据的list的id才是key
            String key = type + "_" + priority;
            log.info("从redis中获取审核文章任务：key:{}",key);
            //1.从redis中获取任务数据 pop （不知道是消费失败还是成功，都把任务删了？？？？？失败也没关系，会有定时任务把没消费的任务从数据库中重新拉取到redis中）
            String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key);
            //lRightPop是从列表的右侧删除最后一个元素()，并将该元素返回。如果列表为空，则该命令返回 nil（或相应的空值）
            //先进先出”（FIFO）原则的队列，通常会选择从一端（如左侧）插入数据，而从另一端（右侧）移除数据。
            //左边入队（即添加任务或数据）通常表示较早的任务或数据，而右边出队（即消费任务或数据）则意味着需要优先处理后添加的任务或数据。
            // 这种设计用于确保及时处理最近添加的任务，以避免长时间等待的情况。
            log.info("redis里现在有审核文章的任务吗{}",task_json);
            if(StringUtils.isNotBlank(task_json)){
                //2.如果获取到任务的json串，转成task对象
                task = JSON.parseObject(task_json, Task.class);
                //更新数据库
                updateDb(task.getTaskId(),ScheduleConstants.EXECUTED);//状态改为 已执行
            }
            //消费任务不是从zset中获取任务，消费任务只能在list中取，zset中的任务(未来任务数据队列)会被定时刷新到list中
            //TODO 这个"定时"体现在 refreshCache方法
        } catch (Exception e) {
            e.printStackTrace();
            log.error("消费任务失败",e);
        }
        //如果获取到任务，更新数据库，更新任务日志，返回任务
        return task;
    }

    /**
     * 删除redis中的任务数据（由于网络不通，使用延迟任务进行重试，无解，后取消任务）
     * @param task
     */
    private void removeTaskFromCache(Task task) {
        //通过key来删除
        String key = task.getTaskType() + "_" + task.getTaskId();
        //因为System.currentTimeMillis()是时间点，所以task.getExecuteTime()也是时间点
        //如果任务的执行时间小于等于当前时间，说明是要立即执行的任务，存入List，所以要删除List中的数据
        if(task.getExecuteTime() <= System.currentTimeMillis()) {//（由于网络不通，使用延迟任务进行重试，无解，后取消任务）
            //TODO 添加的任务的执行时间为什么会小于等于当前时间？？？过去的任务？？
            //(说明已经到了/过了约定要执行的时间)，(是要立即执行的任务)，(先存入redis的list中等着，就等其它微服务过来消费任务)
            //TODO 这个删除任务为什么还是用这个判断条件？？？
            //因为<=当前时间的任务(本应执行却未执行)，肯定在redis的list中
            // (zSet中的大于当前时间但是执行时间还没到预设执行时间的任务，会定时添加到redis的list中)

//由于(你(当前时间)已达到当前要删除的任务的预定执行时间)task.getExecuteTime() <= System.currentTimeMillis()
//所以这个任务一定在redis的list中，所以直接在list中找到并删除即可

            //要删除的是list中的数据
            //lRemove 命令用于从列表中删除指定数量的特定值。
            //lRightPop 命令用于从列表的右端（尾部）弹出一个元素（即删除并返回它）
            cacheService.lRemove(ScheduleConstants.TOPIC+key,0, JSON.toJSONString(task));
            //0 指定了要移除列表中与 JSON.toJSONString(task) 匹配的所有元素，一般只有一个

//            LPUSH mylist "a"
//            LPUSH mylist "b"
//            LPUSH mylist "a"
//            LPUSH mylist "c"
//            LPUSH mylist "a"
//
//            LREM mylist 2 "a"  # 移除前两个 "a"

//            当 index > 0 时:
//            从列表的前面开始（头部）移除 index 个匹配的 value 值。

//            当 index < 0 时:
//            从列表的后面开始（尾部）移除 -index 个匹配的 value 值。

        }else{// (zSet中的大于当前时间但是执行时间还没到预设执行时间的任务，会定时添加到redis的list中)

//由于(你(当前时间)未达到当前要删除的任务的预定执行时间)task.getExecuteTime() > System.currentTimeMillis()
//所以这个任务一定在redis的zSet中，所以直接在zSet中找到并删除即可


            //删除未来要执行的任务(取消任务计划？还没到达重试阈值就取消了？？？)，大于当前时间，存在zset中
            //要删除的是zset中的数据(当前时间 <(已经过了) 执行时间 <=(还没有到) 预设执行时间)
            cacheService.zRemove(ScheduleConstants.FUTURE+key,JSON.toJSONString(task));
        }
    }

    /**
     * 删除任务，更新任务日志
     * @param taskId
     * @param status
     * @return
     */
    private Task updateDb(long taskId, int status) {

        Task task = null;

        try {
            //删除任务，updateDb是放在poll消费任务里的，消费完任务之后，lRightPop会删除redis里对应任务数据的缓存，deleteById也会删除数据库DB里的任务
            taskinfoMapper.deleteById(taskId);
            //更新任务日志
            TaskinfoLogs taskinfoLogs = taskinfoLogsMapper.selectById(taskId);
            //因为当前日志的id和taskId是一致的(在addTaskToDB里的task.setTaskId(taskinfo.getTaskId()))，所以直接用taskId即可
            taskinfoLogs.setStatus(status);
            taskinfoLogsMapper.updateById(taskinfoLogs);

            task = new Task();
            BeanUtils.copyProperties(taskinfoLogs,task);
            task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime());//.getTime()拿到毫秒值long型
        } catch (Exception e) {
            log.error("更新任务失败！任务ID: {}", taskId);
            e.printStackTrace();
        }
        return task;
    }

    @Autowired
    private CacheService cacheService;

    /**
     * 添加任务到redis中
     * @param task
     */
    private void addTaskToCache(Task task) {

        String key = task.getTaskType() + "_" + task.getPriority();

        //获取5分钟之后的时间 毫秒值
        //获得一个表示当前日期和时间的 Calendar 实例
        Calendar calendar = Calendar.getInstance();
        System.out.println("当前时间是：" + calendar);
        calendar.add(Calendar.MINUTE,5);
        System.out.println("5分钟之后是：" + calendar);
        //获得时间毫秒值
        long nextScheduleTime = calendar.getTimeInMillis();

        //2.1如果任务的执行时间小于等于当前时间(说明已经到了执行时间)，(是要立即执行的任务)，(就等其它微服务过来消费任务)存入List中
        if(task.getExecuteTime() <= System.currentTimeMillis()){//因为System.currentTimeMillis()是时间点，所以task.getExecuteTime()也是时间点
            //ScheduleConstants.TOPIC 当前数据key前缀topic_，future_未来数据key前缀ScheduleConstants.FUTURE
            cacheService.lLeftPush(ScheduleConstants.TOPIC+key, JSON.toJSONString(task));//从左边入队，添加到list中
        }else if(task.getExecuteTime() <= nextScheduleTime){

            //第一个if已经过滤掉任务的执行时间小于等于当前时间的任务，
            // 只剩下任务的执行时间大于当前时间的任务，所以不需要再加多一个判断了

            //2.2如果任务的执行时间大于当前时间&&小于等于预设时间（未来5分钟），说明是延迟执行的任务，存入zset（大于预设时间，。。。。？？？）
            //zSet里需要分值，用来排序，就是当前任务执行的时间的毫秒值：task.getExecuteTime()
            cacheService.zAdd(ScheduleConstants.FUTURE+key,JSON.toJSONString(task),task.getExecuteTime());
        }
        //如果这个任务的执行时间 > 未来5分钟(5分钟之后执行)，(刚存入时的时间+5分钟都还没达到执行时间时)
        // 先存入数据库，但不会立即从这个方法存入redis，而是在下面的clearCache()方法里定时处理(同样也是等到时间符合才存入redis中)
    }

    @Autowired
    private TaskinfoMapper taskinfoMapper;

    @Autowired
    private TaskinfoLogsMapper taskinfoLogsMapper;

    /**
     * 添加任务到数据库中
     * @param task(前端传来的)
     * @return
     */
    private boolean addTaskToDB(Task task) {//Task在DTO中，表示前端传来的，Taskinfo在POJO中，表示数据库中的

        boolean flag = false;

        try {
            //保存任务表
            Taskinfo taskinfo = new Taskinfo();
            BeanUtils.copyProperties(task,taskinfo);
            //执行时间这个字段不一样，task的是long型(前端传来的，方便查询？？)，taskinfo的是Date型，需要单独处理
            taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
            taskinfoMapper.insert(taskinfo);

//    如果你使用 @TableId(type = IdType.AUTO)，在插入后，数据库会生成一个自增 ID，MyBatis-Plus 会将这个 ID 填充到 taskinfo 的 id 字段。
//
//    如果你使用 @TableId(type = IdType.ID_WORKER)，在插入操作前，应用逻辑会生成一个 ID（通常是在服务层）。在此情况下，MyBatis-Plus 会使用这个 ID。
//
//    在调用 insert() 方法后，通过 taskinfo.getId() 读取的 ID 总是从 MyBatis-Plus 的内部机制获得，而由哪种策略确定 ID 的具体生成方式则取决于你在 @TableId 中的设置。

            //前提：确保主键字段在数据库中是自增的（通常是数据库中定义为自增主键）
            //1.MyBatis-Plus 内置的插入方法会在执行 SQL 插入操作后，
            //    通过 JDBC 的 getGeneratedKeys() 功能来获得数据库生成的主键。
            //    这意味着无论你使用什么类型的 ID 策略，只要你调用 insert() 方法，
            //    MyBatis-Plus 都会在插入后尝试将生成的 ID 填充回实体对象的相应属性。
            // 而不需要你手动设置 useGeneratedKeys = true 和 keyProperty = "Id"。

            //2.xml文件的话要返回主键id需要：<insert id="insert" useGeneratedKeys="true" keyProperty="taskId">

            //3.如果使用注解方式，插入方法的定义可能会如下，例如：
            //@Insert("INSERT INTO taskinfo (other_columns) VALUES (other_values)")
            //@Options(useGeneratedKeys = true, keyProperty = "taskId")
            //void insert(TaskInfo taskinfo);

            //设置taskId，上面保存了taskinfo之后，数据库就会返回一个自增的id，赋值给taskinfo

            task.setTaskId(taskinfo.getTaskId());

            //保存任务日志表
            TaskinfoLogs taskinfoLogs = new TaskinfoLogs();
            BeanUtils.copyProperties(taskinfo,taskinfoLogs);
            taskinfoLogs.setVersion(1); //初始化乐观锁的版本号为1
            //也可以用枚举
            taskinfoLogs.setStatus(ScheduleConstants.SCHEDULED); //初始化状态为0 初始化 1 执行成功 2 取消
            taskinfoLogsMapper.insert(taskinfoLogs);

            flag = true;//保存成功
        } catch (Exception e) {
            e.printStackTrace();
//            return flag;
        }
        return flag;
    }

    /**
     * 每分钟刷新zset(未来数据)缓存数据到
     * list(当前已经到执行时间了，需要被消费的任务数据)缓存中
     */
    @Scheduled(cron = "0 */1 * * * ?")//如果想让@Scheduled注解生效，需要在启动(引导)类中加注解@EnableScheduling开启调度任务
    // 需要配置一个定时任务线程池，在application.yml中配置即可
    public void refreshCache() {

        //获取锁（作用：使ScheduleApplication-51701/51702等多个服务交替执行refreshCache，而不是同时执行refreshCache）
        String token = cacheService.tryLock("FUTURE_TASK_SYNC",
                                            1000 * 30); //30秒后释放锁

        //如果ScheduleApplication-51701获取锁成功，就会执行下面的代码，否则不执行，
        //避免了多台ScheduleApplication-51702再次执行下面的代码，而导致数据重复消费
        if(StringUtils.isNotBlank(token)){
            log.info("获取锁成功");
            log.info("开始刷新zSet中的未来数据到list中当前待消费任务缓存中...");

            //1.获取所有未来时间数据的集合key
            Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");

            //2.按照key和分值查询符合条件的数据
            for (String futureKey : futureKeys) { //future_100_50
                //获取当前数据的key topic，使用 .split("future_") 后，结果数组将为：["", "100_50"]
                String topicKey = ScheduleConstants.TOPIC + futureKey.split(ScheduleConstants.FUTURE)[1];
                //把future_100_50截掉前面的future_，剩下的就是100_50，[0]就是100，[1]就是100_50

                //2.1获取分值最小的数据，也就是未来时间最小的数据 //从0开始查到 <= 当前时间
                Set<String> tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());
                //3.(用redis管道技术)同步数据
                if(!tasks.isEmpty()){
                    cacheService.refreshWithPipeline(futureKey,topicKey,tasks);
                    log.info("成功地将redis的zSet中"+futureKey+"中的数据同步到redis的list的"+topicKey+"中");
                }
            }

        }
    }
    //数据库任务定时同步到redis
    //同步的时候，是查询小于未来5分钟的所有任务，如果当前缓存中还有没有被消费的任务，这时候有可能这个任务会重复，
    // 所以先要做一个清理缓存中数据的操作（因为数据库中存的是总的数据(list+zSet)）

    /**
     * 数据库任务定时同步到redis（先清理缓存数据，再查询，防止脏数据(防止第一批进入redis的数据在第二批数据进入时再次进入redis)
     * 所以要先把第一批进入redis的数据(有一部分可能被消费后删掉了)清理掉，再查询，防止第一批和第二批存入redis的数据重复）
     */
    //每五分钟执行一次
    @PostConstruct //当前微服务一启动，就会先运行一遍这个方法(开局先同步一次)
    @Scheduled(cron = "0 0/5 * * * ?")
    public void reloadData(){
        //1.清理缓存中的数据 list中待消费的数据，zSet中的未来数据
        clearCache();

        //2.从数据库中查询未来任务数据：执行时间<=未来5分钟的任务数据
        // 第一批进入redis的数据：(刚addTask存入数据库时(当前时间<执行时间<=未来5分钟)和(执行时间<=当前时间)的任务是立即从数据库直接存入list的)
        // 第二批进入redis的数据：(刚addTask存入数据库时(未来5分钟<执行时间)的任务就先存入数据库，还没能够存入redis，等到当前的时间流逝以满足(执行时间<=未来5分钟)之后
        // 就会被当前的定时方法存入redis中，然后从zSet流到list中，然后从右边被poll取出来消费)

        //获得一个表示当前日期和时间未来5分钟的 Calendar 实例
        Calendar calendar = Calendar.getInstance();
        System.out.println("当前时间是：" + calendar);
        calendar.add(Calendar.MINUTE,5);
        //数据库中的 执行时间<=未来5分钟的任务数据 已经在addTask中存入数据库了
        List<Taskinfo> taskinfoList = taskinfoMapper.selectList(Wrappers.<Taskinfo>lambdaQuery().le(Taskinfo::getExecuteTime, calendar.getTime()));

        //3.把任务添加到redis中
        if(taskinfoList != null && taskinfoList.size() > 0){
            for (Taskinfo taskinfo : taskinfoList) {
                Task task = new Task();
                BeanUtils.copyProperties(taskinfo,task);
                //属性类型不同，需要单独转换
                task.setExecuteTime(taskinfo.getExecuteTime().getTime());
                addTaskToCache(task);
            }
        }
        log.info("数据库同步到redis");

    }

    /**
     * 清理redis缓存中的所有数据
     * (list中的topic数据，zSet中的future数据)
     */
    //不用担心删除redis里的所有缓存后，再次重新加载数据库DB里的任务数据到redis里会造成redis里任务重复，因为任务一旦从list的右边出队(poll)被消费了，
    // lRightPop会删除redis里对应的已被消费了的任务数据的缓存，deleteById也会删除数据库DB里对应的已被消费了的任务数据
    public void clearCache(){
        //先查询当前的所有的topicKey和futureKey
        Set<String> topicKeys = cacheService.scan(ScheduleConstants.TOPIC + "*");
        Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");
        cacheService.delete(topicKeys);
        cacheService.delete(futureKeys);
    }


}
